using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using DslCrossModelFramework.NameResolution;
using Microsoft.VisualStudio.Modeling;

namespace DslCrossModelFramework.ModelTraversal {


    /// <summary>
    /// Defines the direction to follow in a ModelPath link segment
    /// </summary>
    public enum ModelPathLinkDirection {
        /// <summary>
        /// Direction does not apply to the model element type
        /// </summary>
        NotApplicable,
        /// <summary>
        /// Direction is not known
        /// </summary>
        Unknown,
        /// <summary>
        /// Direction flows from source to target
        /// </summary>
        SourceToTarget,
        /// <summary>
        /// Direction flows from target to source
        /// </summary>
        TargetToSource
    }

    /// <summary>
    /// Provides functionality to represent traversal of paths through a domain model
    /// </summary>
    [System.ComponentModel.TypeConverter(typeof(ModelPathSerializationTypeConverter))]
    public class ModelPath : LinkedList<IModelPathSegment>, ICloneable {

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelPath"/> class.
        /// </summary>
        public ModelPath() {
            
        }

        public ModelPath(ModelElement startElement) {
            this.Add(startElement);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelPath"/> class.
        /// </summary>
        /// <param name="scope">The scope.</param>
        public ModelPath(string scope) {
            this.scope = scope;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelPath"/> class.
        /// </summary>
        /// <param name="parent">The parent.</param>
        /// <param name="child">The child.</param>
        /// <param name="scope">The scope.</param>
        public ModelPath(ModelElement parent, ModelElement child, string scope) {
            ModelPath modelPath = ModelPathHelper.GetLinkFromParentToChild(parent, child);
            foreach (IModelPathSegment segment in modelPath) {
                this.AddLast(segment);
            }
        }

        /// <summary>
        /// Adds the specified next model element.
        /// </summary>
        /// <param name="nextModelElement">The next model element.</param>
        public void Add(ModelElement nextModelElement) {
            CrossModelManager manager = new CrossModelManager(nextModelElement.Store);
            if(this.Count > 0) {
                ModelElement previous = this.Last.Value.ModelElement;
                // This is not the first element so we need to make some checks
                if(nextModelElement is ElementLink) {
                    // The new element is a link so make sure it links to or from the previous element
                    if(!((ElementLink)nextModelElement).LinkedElements.Contains(previous)){
                        // The link does not link the previous element so throw an exception
                        throw new InvalidLinkInModelPathException(previous, nextModelElement as ElementLink);
                    }
                    bool rolePlayersAreSame = true;
                    foreach(ModelElement linkedElement in ((ElementLink)nextModelElement).LinkedElements) {
                        if(linkedElement != previous) {
                            rolePlayersAreSame = false;
                        }
                    }
                    if(rolePlayersAreSame) {
                        throw new AmbiguousLinkDirectionException(previous);
                    }
                    bool isSource = true;
                    foreach (DomainRoleInfo role in ((ElementLink)nextModelElement).GetDomainRelationship().DomainRoles) {
                        if(role.GetElementLinks(previous).Contains((ElementLink)nextModelElement)) {
                            isSource = role.IsSource;
                        }
                    }

                    this.AddLast(
                        new ModelElementLinkPathSegment(nextModelElement as ElementLink,
                                                        isSource
                                                            ? ModelPathLinkDirection.SourceToTarget
                                                            : ModelPathLinkDirection.TargetToSource,
                                                        manager));
                }
                else {
                    if (this.Count < 2 || !(previous is ElementLink)) {
                        // try to create a new path from the previous and merge
                        ModelPath newModelPath = new ModelPath(this.Last.Value.ModelElement, nextModelElement, scope);
                        this.Merge(newModelPath);
                        //throw new InvalidModelElementInModelPath(nextModelElement);
                    }
                    else {
                        // Check that the previous element is a link and that it links this and the other side
                        ModelElement otherSideOfLink = this.Last.Previous.Value.ModelElement;
                        if (!((ElementLink) previous).LinkedElements.Contains(otherSideOfLink) ||
                            !((ElementLink) previous).LinkedElements.Contains(nextModelElement)) {
                            throw new InvalidModelElementInModelPath(nextModelElement);
                        }
                        this.AddLast(
                            new ModelElementPathSegment(nextModelElement, manager)
                            );
                    }
                }
            }
            else {
                this.AddLast(new ModelElementPathSegment(nextModelElement, manager));
            }
        }

        /// <summary>
        /// Adds the specified link.
        /// </summary>
        /// <param name="link">The link.</param>
        /// <param name="direction">The direction.</param>
        public void Add(ElementLink link, ModelPathLinkDirection direction) {
            if (this.Count == 0) {
                throw new InvalidLinkInModelPathException(null, link);
            }

            CrossModelManager manager = new CrossModelManager(link.Store);
            ModelElement previous = this.Last.Value.ModelElement;
            if (!(link.LinkedElements.Contains(previous))) {
                // The link does not link the previous element so throw an exception
                throw new InvalidLinkInModelPathException(previous, link);
            }

            this.AddLast(
                new ModelElementLinkPathSegment(link,
                                                direction,
                                                manager));
        }


        /// <summary>
        /// Merges the specified path onto the end of the current path.
        /// </summary>
        /// <param name="childPath">The child path.</param>
        public void Merge(ModelPath childPath) {
            try {
                ModelPath newChildPath = childPath;
                if(Last.Value.ModelElement != childPath.First.Value.ActualModelElement) {
                    // Try to find a join between the 2 paths as their last and first elements don't match
                        ModelPath joinPath = new ModelPath(Last.Value.ModelElement, childPath.First.Value.ActualModelElement, string.Empty);
                        AddLast(joinPath.First.Next.Value);
                }
                LinkedListNode<IModelPathSegment> currentPath = childPath.First;
                while(currentPath.Next != null) {
                    AddLast(currentPath.Next.Value);
                    currentPath = currentPath.Next;
                }
            }
            catch (Exception e) {
                throw new ModelPathMergeException("Could not merge Model Paths", e);
            }
        }


        /// <summary>
        /// Determines whether the current path starts with the specified path.
        /// </summary>
        /// <param name="modelPath">The model path.</param>
        /// <returns><c>true</c> if the instance starts with the specified path; otherwise, <c>false</c>.</returns>
        public bool StartsWith(ModelPath modelPath) {
            if(modelPath.Count == 0) {
                return true;
            }
            if(modelPath.Count > this.Count || this.Count == 0 ) {
                return false;
            }

            LinkedListNode<IModelPathSegment> currentActualSegment = this.First;
            LinkedListNode<IModelPathSegment> currentCompareSegment = modelPath.First;

            while(currentCompareSegment != null) {
                if(currentCompareSegment.Value.GetSerializedString() != currentActualSegment.Value.GetSerializedString()) {
                    return false;
                }
                currentCompareSegment = currentCompareSegment.Next;
                currentActualSegment = currentActualSegment.Next;
            }

            return true;
        }

        /// <summary>
        /// Gets the source model element.
        /// </summary>
        /// <value>The source model element.</value>
        public ModelElement SourceModelElement {
            get { 
                if(this.Count > 0) {
                    return this.First.Value.ModelElement;
                }
                return null;
            }
        }

        /// <summary>
        /// Gets the target model element.
        /// </summary>
        /// <value>The target model element.</value>
        public ModelElement TargetModelElement {
            get {
                
                if (this.Count > 0) {
                    if (!this.Last.Value.ModelFileInfo.IsLoaded) {
                        this.Last.Value.ModelFileInfo.Load();
                    }
                    return this.Last.Value.ModelElement;
                }
                return null;
            }
        }


        /// <summary>
        /// Gets the scope.
        /// </summary>
        /// <value>The scope.</value>
        public string Scope {
            get{ return scope;}
        }
        private string scope = string.Empty;

        #region Serialization

        public string SerializeToString() {
            //TODO: Implement Serialization
            List<string> paths = new List<string>();
            foreach (IModelPathSegment segment in this) {
                paths.Add(segment.GetSerializedString());
            }
            return string.Join(@"/", paths.ToArray());
        }

        public static ModelPath DeserializeFromString(string value, CrossModelManager manager) {
            ModelPath deserializedModelPath = new ModelPath();
            try {
                if(string.IsNullOrEmpty(value)) {
                    return new ModelPath();
                }

                List<string> paths = new List<string>();
                Regex regex = new Regex(@"\x5B[^\]]*\x5D");
                foreach(Match match in regex.Matches(value)) {
                    deserializedModelPath.AddLast(GetSegmentFromSerializationString(match.Value, manager));
                }
                
            }
            catch (Exception e) {
                throw new ModelPathDeserializationException(string.Format("Could not deserialize model path: '{0}'", e.Message),e);
            }
            return deserializedModelPath;
        }

        private static IModelPathSegment GetSegmentFromSerializationString(string value, CrossModelManager manager) {
            IModelPathSegment returnValue = null;
            value = value.TrimStart('[');
            value = value.TrimEnd(']');
            string[] vars = value.Split('|');
            switch (vars[0].TrimStart('[').TrimEnd(']')) {
                case "Link":
                    returnValue = new ModelElementLinkPathSegment(string.Format("[{0}]",value), manager);
                    break;
                case "Element":
                    returnValue = new ModelElementPathSegment(string.Format("[{0}]", value), manager);
                    break;
            }

            return returnValue;
        }

        #endregion


        /// <summary>
        /// Returns a <see cref="T:System.String"/> that represents the current <see cref="ModelPath"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"/> that represents the current <see cref="ModelPath"/>.
        /// </returns>
        public override string ToString() {
            return ToString(scope, ".");
        }


        /// <summary>
        /// Returns a <see cref="T:System.String"/> that represents the current <see cref="ModelPath"/>.
        /// </summary>
        /// <param name="scope">The scope.</param>
        /// <returns>
        /// A <see cref="T:System.String"/> that represents the current <see cref="ModelPath"/>.
        /// </returns>
        public virtual string ToString(string scope) {
            return ToString(scope, ".");
        }

        /// <summary>
        /// Returns a <see cref="T:System.String"/> that represents the current <see cref="ModelPath"/>.
        /// </summary>
        /// <param name="scope">The scope.</param>
        /// <param name="separator">The separator.</param>
        /// <returns>
        /// A <see cref="T:System.String"/> that represents the current <see cref="ModelPath"/>.
        /// </returns>
        public virtual string ToString(string scope, string separator) {
            List<string> namePaths = new List<string>();
            LinkedListNode<IModelPathSegment> currentNode = First;
            while (currentNode != null) {
                namePaths.Add(NameResolutionManager.Current.GetResolver(currentNode.Value.ModelElement, scope).GetName(currentNode));
                currentNode = currentNode.Next;
            }
            return string.Join(separator, namePaths.ToArray());
        }

        public object Clone() {
            ModelPath clone = new ModelPath();
            foreach (IModelPathSegment segment in this) {
                clone.AddLast(segment);
            }
            return clone;
        }
    }

    #region Custom Path Exceptions

    public class ModelPathDeserializationException : Exception {
        public ModelPathDeserializationException(string message) : base(message) {
        }

        public ModelPathDeserializationException(string message, Exception inner) : base(message, inner) {
        }
    }

    public class AmbiguousLinkDirectionException : Exception {
        public AmbiguousLinkDirectionException(ModelElement parent)
            : base(
                string.Format(
                    "Cannot determine the direction of the link added to the Model Path as the element {0} is linked by it more than once",
                    NameResolutionManager.Current.GetElementName(parent))) {
        }
    }

    public class InvalidLinkInModelPathException : Exception {
        public InvalidLinkInModelPathException(ModelElement previous, ElementLink link)
            : base(
                string.Format(
                    "An Invalid link was added to the model path, the previous element {0} does not play a role in the link {1}",
                    previous == null ? "None" : NameResolutionManager.Current.GetElementName(previous), link.Id)) { }
    }

    public class InvalidModelElementInModelPath : Exception {
        public InvalidModelElementInModelPath(ModelElement element) : base(string.Format("An Invalid model element '{0}' was added to a path make sure the previous element is a link and that it links the new element and the element before the link", NameResolutionManager.Current.GetElementName(element))) {
        }
    }

    public class ModelPathMergeException : Exception {
        public ModelPathMergeException(string message) : base(message) {}

        public ModelPathMergeException(string message, Exception inner) : base(message, inner) {}
    }

    #endregion

}